//========================================================================
// GLFW 3.3 X11 - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2002-2006 Marcus Geelnard
// Copyright (c) 2006-2019 Camilla Löwy <elmindreda@glfw.org>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
//    claim that you wrote the original software. If you use this software
//    in a product, an acknowledgment in the product documentation would
//    be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
//    be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
//    distribution.
//
//========================================================================
// It is fine to use C99 in this file because it will not be built with VS
//========================================================================

#include "internal.h"

#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// Check whether the display mode should be included in enumeration
//
static GLFWbool modeIsGood(const XRRModeInfo* mi) {
  return (mi->modeFlags & RR_Interlace) == 0;
}

// Calculates the refresh rate, in Hz, from the specified RandR mode info
//
static int calculateRefreshRate(const XRRModeInfo* mi) {
  if (mi->hTotal && mi->vTotal)
    return (int)round((double)mi->dotClock / ((double)mi->hTotal * (double)mi->vTotal));
  else
    return 0;
}

// Returns the mode info for a RandR mode XID
//
static const XRRModeInfo* getModeInfo(const XRRScreenResources* sr, RRMode id) {
  for (int i = 0; i < sr->nmode; i++) {
    if (sr->modes[i].id == id)
      return sr->modes + i;
  }

  return NULL;
}

// Convert RandR mode info to GLFW video mode
//
static GLFWvidmode vidmodeFromModeInfo(const XRRModeInfo* mi,
                                       const XRRCrtcInfo* ci) {
  GLFWvidmode mode;

  if (ci->rotation == RR_Rotate_90 || ci->rotation == RR_Rotate_270) {
    mode.width = mi->height;
    mode.height = mi->width;
  } else {
    mode.width = mi->width;
    mode.height = mi->height;
  }

  mode.refreshRate = calculateRefreshRate(mi);

  _glfwSplitBPP(DefaultDepth(_glfw.x11.display, _glfw.x11.screen),
                &mode.redBits,
                &mode.greenBits,
                &mode.blueBits);

  return mode;
}

//////////////////////////////////////////////////////////////////////////
//////                       GLFW internal API                      //////
//////////////////////////////////////////////////////////////////////////

// Poll for changes in the set of connected monitors
//
void _glfwPollMonitorsX11(void) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    int disconnectedCount, screenCount = 0;
    _GLFWmonitor** disconnected = NULL;
    XineramaScreenInfo* screens = NULL;
    XRRScreenResources* sr = XRRGetScreenResourcesCurrent(_glfw.x11.display,
                                                          _glfw.x11.root);
    RROutput primary = XRRGetOutputPrimary(_glfw.x11.display,
                                           _glfw.x11.root);

    if (_glfw.x11.xinerama.available)
      screens = XineramaQueryScreens(_glfw.x11.display, &screenCount);

    disconnectedCount = _glfw.monitorCount;
    if (disconnectedCount) {
      disconnected = calloc(_glfw.monitorCount, sizeof(_GLFWmonitor*));
      memcpy(disconnected,
             _glfw.monitors,
             _glfw.monitorCount * sizeof(_GLFWmonitor*));
    }

    for (int i = 0; i < sr->noutput; i++) {
      int j, type, widthMM, heightMM;

      XRROutputInfo* oi = XRRGetOutputInfo(_glfw.x11.display, sr, sr->outputs[i]);
      if (oi->connection != RR_Connected || oi->crtc == None) {
        XRRFreeOutputInfo(oi);
        continue;
      }

      for (j = 0; j < disconnectedCount; j++) {
        if (disconnected[j] &&
            disconnected[j]->x11.output == sr->outputs[i]) {
          disconnected[j] = NULL;
          break;
        }
      }

      if (j < disconnectedCount) {
        XRRFreeOutputInfo(oi);
        continue;
      }

      XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, oi->crtc);
      if (ci->rotation == RR_Rotate_90 || ci->rotation == RR_Rotate_270) {
        widthMM = oi->mm_height;
        heightMM = oi->mm_width;
      } else {
        widthMM = oi->mm_width;
        heightMM = oi->mm_height;
      }

      if (widthMM <= 0 || heightMM <= 0) {
        // HACK: If RandR does not provide a physical size, assume the
        //       X11 default 96 DPI and calculate from the CRTC viewport
        // NOTE: These members are affected by rotation, unlike the mode
        //       info and output info members
        widthMM = (int)(ci->width * 25.4f / 96.f);
        heightMM = (int)(ci->height * 25.4f / 96.f);
      }

      _GLFWmonitor* monitor = _glfwAllocMonitor(oi->name, widthMM, heightMM);
      monitor->x11.output = sr->outputs[i];
      monitor->x11.crtc = oi->crtc;

      for (j = 0; j < screenCount; j++) {
        if (screens[j].x_org == ci->x &&
            screens[j].y_org == ci->y &&
            ((unsigned int)screens[j].width) == ci->width &&
            ((unsigned int)screens[j].height) == ci->height) {
          monitor->x11.index = j;
          break;
        }
      }

      if (monitor->x11.output == primary)
        type = _GLFW_INSERT_FIRST;
      else
        type = _GLFW_INSERT_LAST;

      _glfwInputMonitor(monitor, GLFW_CONNECTED, type);

      XRRFreeOutputInfo(oi);
      XRRFreeCrtcInfo(ci);
    }

    XRRFreeScreenResources(sr);

    if (screens)
      XFree(screens);

    for (int i = 0; i < disconnectedCount; i++) {
      if (disconnected[i])
        _glfwInputMonitor(disconnected[i], GLFW_DISCONNECTED, 0);
    }

    free(disconnected);
  } else {
    const int widthMM = DisplayWidthMM(_glfw.x11.display, _glfw.x11.screen);
    const int heightMM = DisplayHeightMM(_glfw.x11.display, _glfw.x11.screen);

    _glfwInputMonitor(_glfwAllocMonitor("Display", widthMM, heightMM),
                      GLFW_CONNECTED,
                      _GLFW_INSERT_FIRST);
  }
}

// Set the current video mode for the specified monitor
//
void _glfwSetVideoModeX11(_GLFWmonitor* monitor, const GLFWvidmode* desired) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    GLFWvidmode current;
    RRMode native = None;

    const GLFWvidmode* best = _glfwChooseVideoMode(monitor, desired);
    _glfwPlatformGetVideoMode(monitor, &current);
    if (_glfwCompareVideoModes(&current, best) == 0)
      return;

    XRRScreenResources* sr =
        XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
    XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);
    XRROutputInfo* oi = XRRGetOutputInfo(_glfw.x11.display, sr, monitor->x11.output);

    for (int i = 0; i < oi->nmode; i++) {
      const XRRModeInfo* mi = getModeInfo(sr, oi->modes[i]);
      if (!modeIsGood(mi))
        continue;

      const GLFWvidmode mode = vidmodeFromModeInfo(mi, ci);
      if (_glfwCompareVideoModes(best, &mode) == 0) {
        native = mi->id;
        break;
      }
    }

    if (native) {
      if (monitor->x11.oldMode == None)
        monitor->x11.oldMode = ci->mode;

      XRRSetCrtcConfig(_glfw.x11.display,
                       sr,
                       monitor->x11.crtc,
                       CurrentTime,
                       ci->x,
                       ci->y,
                       native,
                       ci->rotation,
                       ci->outputs,
                       ci->noutput);
    }

    XRRFreeOutputInfo(oi);
    XRRFreeCrtcInfo(ci);
    XRRFreeScreenResources(sr);
  }
}

// Restore the saved (original) video mode for the specified monitor
//
void _glfwRestoreVideoModeX11(_GLFWmonitor* monitor) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    if (monitor->x11.oldMode == None)
      return;

    XRRScreenResources* sr =
        XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
    XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);

    XRRSetCrtcConfig(_glfw.x11.display,
                     sr,
                     monitor->x11.crtc,
                     CurrentTime,
                     ci->x,
                     ci->y,
                     monitor->x11.oldMode,
                     ci->rotation,
                     ci->outputs,
                     ci->noutput);

    XRRFreeCrtcInfo(ci);
    XRRFreeScreenResources(sr);

    monitor->x11.oldMode = None;
  }
}

//////////////////////////////////////////////////////////////////////////
//////                       GLFW platform API                      //////
//////////////////////////////////////////////////////////////////////////

void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor) {
}

void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor, int* xpos, int* ypos) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    XRRScreenResources* sr =
        XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
    XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);

    if (ci) {
      if (xpos)
        *xpos = ci->x;
      if (ypos)
        *ypos = ci->y;

      XRRFreeCrtcInfo(ci);
    }

    XRRFreeScreenResources(sr);
  }
}

void _glfwPlatformGetMonitorContentScale(_GLFWmonitor* monitor,
                                         float* xscale, float* yscale) {
  if (xscale)
    *xscale = _glfw.x11.contentScaleX;
  if (yscale)
    *yscale = _glfw.x11.contentScaleY;
}

void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor, int* xpos, int* ypos, int* width, int* height) {
  int areaX = 0, areaY = 0, areaWidth = 0, areaHeight = 0;

  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    XRRScreenResources* sr =
        XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
    XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);

    areaX = ci->x;
    areaY = ci->y;

    const XRRModeInfo* mi = getModeInfo(sr, ci->mode);

    if (ci->rotation == RR_Rotate_90 || ci->rotation == RR_Rotate_270) {
      areaWidth = mi->height;
      areaHeight = mi->width;
    } else {
      areaWidth = mi->width;
      areaHeight = mi->height;
    }

    XRRFreeCrtcInfo(ci);
    XRRFreeScreenResources(sr);
  } else {
    areaWidth = DisplayWidth(_glfw.x11.display, _glfw.x11.screen);
    areaHeight = DisplayHeight(_glfw.x11.display, _glfw.x11.screen);
  }

  if (_glfw.x11.NET_WORKAREA && _glfw.x11.NET_CURRENT_DESKTOP) {
    Atom* extents = NULL;
    Atom* desktop = NULL;
    const unsigned long extentCount =
        _glfwGetWindowPropertyX11(_glfw.x11.root,
                                  _glfw.x11.NET_WORKAREA,
                                  XA_CARDINAL,
                                  (unsigned char**)&extents);

    if (_glfwGetWindowPropertyX11(_glfw.x11.root,
                                  _glfw.x11.NET_CURRENT_DESKTOP,
                                  XA_CARDINAL,
                                  (unsigned char**)&desktop) > 0) {
      if (extentCount >= 4 && *desktop < extentCount / 4) {
        const int globalX = extents[*desktop * 4 + 0];
        const int globalY = extents[*desktop * 4 + 1];
        const int globalWidth = extents[*desktop * 4 + 2];
        const int globalHeight = extents[*desktop * 4 + 3];

        if (areaX < globalX) {
          areaWidth -= globalX - areaX;
          areaX = globalX;
        }

        if (areaY < globalY) {
          areaHeight -= globalY - areaY;
          areaY = globalY;
        }

        if (areaX + areaWidth > globalX + globalWidth)
          areaWidth = globalX - areaX + globalWidth;
        if (areaY + areaHeight > globalY + globalHeight)
          areaHeight = globalY - areaY + globalHeight;
      }
    }

    if (extents)
      XFree(extents);
    if (desktop)
      XFree(desktop);
  }

  if (xpos)
    *xpos = areaX;
  if (ypos)
    *ypos = areaY;
  if (width)
    *width = areaWidth;
  if (height)
    *height = areaHeight;
}

GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count) {
  GLFWvidmode* result;

  *count = 0;

  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    XRRScreenResources* sr =
        XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
    XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);
    XRROutputInfo* oi = XRRGetOutputInfo(_glfw.x11.display, sr, monitor->x11.output);

    result = calloc(oi->nmode, sizeof(GLFWvidmode));

    for (int i = 0; i < oi->nmode; i++) {
      const XRRModeInfo* mi = getModeInfo(sr, oi->modes[i]);
      if (!modeIsGood(mi))
        continue;

      const GLFWvidmode mode = vidmodeFromModeInfo(mi, ci);
      int j;

      for (j = 0; j < *count; j++) {
        if (_glfwCompareVideoModes(result + j, &mode) == 0)
          break;
      }

      // Skip duplicate modes
      if (j < *count)
        continue;

      (*count)++;
      result[*count - 1] = mode;
    }

    XRRFreeOutputInfo(oi);
    XRRFreeCrtcInfo(ci);
    XRRFreeScreenResources(sr);
  } else {
    *count = 1;
    result = calloc(1, sizeof(GLFWvidmode));
    _glfwPlatformGetVideoMode(monitor, result);
  }

  return result;
}

void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode* mode) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.monitorBroken) {
    XRRScreenResources* sr =
        XRRGetScreenResourcesCurrent(_glfw.x11.display, _glfw.x11.root);
    XRRCrtcInfo* ci = XRRGetCrtcInfo(_glfw.x11.display, sr, monitor->x11.crtc);

    if (ci) {
      const XRRModeInfo* mi = getModeInfo(sr, ci->mode);
      if (mi) // mi can be NULL if the monitor has been disconnected
        *mode = vidmodeFromModeInfo(mi, ci);

      XRRFreeCrtcInfo(ci);
    }

    XRRFreeScreenResources(sr);
  } else {
    mode->width = DisplayWidth(_glfw.x11.display, _glfw.x11.screen);
    mode->height = DisplayHeight(_glfw.x11.display, _glfw.x11.screen);
    mode->refreshRate = 0;

    _glfwSplitBPP(DefaultDepth(_glfw.x11.display, _glfw.x11.screen),
                  &mode->redBits,
                  &mode->greenBits,
                  &mode->blueBits);
  }
}

GLFWbool _glfwPlatformGetGammaRamp(_GLFWmonitor* monitor, GLFWgammaramp* ramp) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.gammaBroken) {
    const size_t size = XRRGetCrtcGammaSize(_glfw.x11.display,
                                            monitor->x11.crtc);
    XRRCrtcGamma* gamma = XRRGetCrtcGamma(_glfw.x11.display,
                                          monitor->x11.crtc);

    _glfwAllocGammaArrays(ramp, size);

    memcpy(ramp->red, gamma->red, size * sizeof(unsigned short));
    memcpy(ramp->green, gamma->green, size * sizeof(unsigned short));
    memcpy(ramp->blue, gamma->blue, size * sizeof(unsigned short));

    XRRFreeGamma(gamma);
    return GLFW_TRUE;
  } else if (_glfw.x11.vidmode.available) {
    int size;
    XF86VidModeGetGammaRampSize(_glfw.x11.display, _glfw.x11.screen, &size);

    _glfwAllocGammaArrays(ramp, size);

    XF86VidModeGetGammaRamp(_glfw.x11.display,
                            _glfw.x11.screen,
                            ramp->size,
                            ramp->red,
                            ramp->green,
                            ramp->blue);
    return GLFW_TRUE;
  } else {
    _glfwInputError(GLFW_PLATFORM_ERROR,
                    "X11: Gamma ramp access not supported by server");
    return GLFW_FALSE;
  }
}

void _glfwPlatformSetGammaRamp(_GLFWmonitor* monitor, const GLFWgammaramp* ramp) {
  if (_glfw.x11.randr.available && !_glfw.x11.randr.gammaBroken) {
    if (((unsigned int)XRRGetCrtcGammaSize(_glfw.x11.display, monitor->x11.crtc)) != ramp->size) {
      _glfwInputError(GLFW_PLATFORM_ERROR,
                      "X11: Gamma ramp size must match current ramp size");
      return;
    }

    XRRCrtcGamma* gamma = XRRAllocGamma(ramp->size);

    memcpy(gamma->red, ramp->red, ramp->size * sizeof(unsigned short));
    memcpy(gamma->green, ramp->green, ramp->size * sizeof(unsigned short));
    memcpy(gamma->blue, ramp->blue, ramp->size * sizeof(unsigned short));

    XRRSetCrtcGamma(_glfw.x11.display, monitor->x11.crtc, gamma);
    XRRFreeGamma(gamma);
  } else if (_glfw.x11.vidmode.available) {
    XF86VidModeSetGammaRamp(_glfw.x11.display,
                            _glfw.x11.screen,
                            ramp->size,
                            (unsigned short*)ramp->red,
                            (unsigned short*)ramp->green,
                            (unsigned short*)ramp->blue);
  } else {
    _glfwInputError(GLFW_PLATFORM_ERROR,
                    "X11: Gamma ramp access not supported by server");
  }
}

//////////////////////////////////////////////////////////////////////////
//////                        GLFW native API                       //////
//////////////////////////////////////////////////////////////////////////

GLFWAPI RRCrtc glfwGetX11Adapter(GLFWmonitor* handle) {
  _GLFWmonitor* monitor = (_GLFWmonitor*)handle;
  _GLFW_REQUIRE_INIT_OR_RETURN(None);
  return monitor->x11.crtc;
}

GLFWAPI RROutput glfwGetX11Monitor(GLFWmonitor* handle) {
  _GLFWmonitor* monitor = (_GLFWmonitor*)handle;
  _GLFW_REQUIRE_INIT_OR_RETURN(None);
  return monitor->x11.output;
}
